/* John Zigler and John Spicher
 * Cryptography, Fall 2011
 * Final Project
 */

package cryptofinal;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;

public class Main implements MouseListener {
	
    //Declare variables
    private String encryptedMessage,
            encryptedMessagePath,
            decryptedMessage,
            dictionaryPath;
    private CharacterNode dictionary;
    private final String ALPHABET = "abcdefghijklmnopqrstuvwxyz";
    private final char[] mostCommon = "ETAONRISHDLFCMUGYPWBVKXJQZ".toCharArray();
    private JTextArea textArea;
    private boolean mouseListenerIsActive;
    
    /* beginDecryption method
     * Initializes dictionary path. This has been hard-coded for testing purposes
     * and could potentially be made dynamic, but that seems outside of the
     * project scope. Loads the dictionary, loads the encrypted message,
     * and begins the decryption sequence.
     */
    public void beginDecryption() {
    	
    	createAndShowGUI();
    	redirectSystemStreams();
    	
        dictionaryPath = "dictionary.txt"; //args[0]; //This is hard-coded for testing purposes
//                "C:"+File.separator+
//                "Users"+File.separator+
//                "John"+File.separator+
//                "Documents"+File.separator+
//                "Homework"+File.separator+
//                "Cryptography"+File.separator+
//                "decrypter"+File.separator+
//                "trunk"+File.separator+
//                "dictionary.txt";
        
        loadDictionary();
        
        loadMessage();
        
        //Initialize decrypted message
        char[] temp = encryptedMessage.toCharArray();
        
        for (int i=0;i<encryptedMessage.length();i++) {
        	
            if (Character.isLetter(encryptedMessage.charAt(i))) {
            	
                temp[i]='_';
                
            }
            
        }
        
        decryptedMessage = String.valueOf(temp);
        
        System.out.println("Decrypting message, this may take some time.");
        System.out.println("To see current decrypted message click anywhere in this window.\n");
        
        long startTime = System.currentTimeMillis();
        
        //Begin decryption sequence
        guesser(mostFrequentChars(encryptedMessage), mostCommon);
        
        mouseListenerIsActive = false;
        
        long endTime = System.currentTimeMillis();
        long timeTaken = (endTime - startTime) / 1000;
        
        System.out.println("Time taken: " + timeTaken + " seconds");
        
    }
    
    /* loadDictionary method
     * Opens the file at dictionaryPath, which is expected to be a text file
     * with one word on each line. Adds each line in the file to the
     * CharacterNode dictionary.
     */
    private void loadDictionary() {
    	
        //System.out.println("Loading dictionary...");
        dictionary = new CharacterNode(' ');
        
        try {
        	
        	FileReader in = new FileReader(dictionaryPath);
//        	InputStreamReader in = new InputStreamReader(//used for jar file
//        			this.getClass().getResourceAsStream("/dictionary.txt"));
            BufferedReader reader = new BufferedReader(in);
            String nextLine = reader.readLine();
            
            while (nextLine!=null) {
            	
                dictionary.add(nextLine);
                nextLine = reader.readLine();
                
            }
            
            reader.close();
            
        }
        catch (IOException e) {
        	
        	System.out.println(
                "Something didn't work while opening "+dictionaryPath);
        	
    	}
        
        //System.out.println("Dictionary loaded");
    }
    
    /* 
     * Prompt the user to choose a file for decryption,
     * then retrieves the data from the file.
     */
    private void loadMessage() {
    	
    	//Get the path of the message to be decrypted
        JFileChooser messageChooser = new JFileChooser();
        
        messageChooser.setDialogTitle("Choose Encrypted Message");
        messageChooser.setFileFilter(
                    new FileNameExtensionFilter("Text files", "txt"));
        
        int returnVal = messageChooser.showOpenDialog(null);
        if(returnVal == JFileChooser.APPROVE_OPTION) {
        	
        	encryptedMessagePath = messageChooser.getSelectedFile().getAbsolutePath();
        	
        }
        else{
        	
        	System.out.println("Something failed while opening message");
        	System.exit(1);
        	
    	}
        
        //Get the message from the file
        String nextLine = null;
        encryptedMessage = "";
        
        try {
        	
        	FileReader in = new FileReader(encryptedMessagePath);
            BufferedReader reader = new BufferedReader(in);
            nextLine = reader.readLine();
            
            while (nextLine!=null) {
            	
            	encryptedMessage+=nextLine+"\n";
                nextLine = reader.readLine();
                
            }
            
            reader.close();
        }
        catch (IOException e) {
        	
        	System.out.println("Something didn't work while opening "+encryptedMessagePath);
        	
    	}
        
        textArea.setText("\nEncrypted Message:\n");
        System.out.println(encryptedMessage);
    	
    }
    
    /* mostFrequentChars method
     * Accepts as parameters the text to analyze. Counts the occurances of each
     * letter within the text, and returns an array of the characters sorted by
     * frequency in descending order.
     */
    private char[] mostFrequentChars(String text) {
    	
        int[] frequency = getFrequency(text);
        char[] inOrder = new char[ALPHABET.length()];
        int maxIndex=-1, maxCount;
        
        for (int i=0;i<inOrder.length;i++) {
        	
            maxCount=-1;
            
            for (int j=0;j<frequency.length;j++) {
            	
                if (frequency[j]>maxCount) {
                	
                    maxIndex=j;
                    maxCount=frequency[j];
                    
                }
                
            }
            
            inOrder[i]=ALPHABET.charAt(maxIndex);
            frequency[maxIndex]=-1;
            
        }
        
//        System.out.println("Results of frequency analysis:");
//        System.out.println(inOrder);
        
        return inOrder;
    }
    
    /* getFrequency method
     * Accepts as parameters the text to analyze. Counts the occurances of each
     * letter within the text. Returns an array of integers, the length of the
     * alphabet, with the integer value in each index representing the number of
     * occurances of the letter at that index in the alphabet.
     */
    private int[] getFrequency(String text) {
    	
        int[] frequency = new int[ALPHABET.length()];
        
        while (!text.equals("")) {
        	
            int x=Character.getNumericValue(text.charAt(0))-10;
            
            if (x>=0 && x<=25) {
            	
            	frequency[x]++;
            	
            }
            
            text=text.substring(1);
            
        }
        
        return frequency;
    }
    
    /* guesser method
     * The workhorse of the solution algorithm. Accepts as parameters an ordered
     * array of the most frequent characters in the ciphertext that have not yet
     * been substituted, and an ordered array of the most common letters in the
     * English language that have not yet been used. These arrays should have
     * the same size. If they are empty, then the entire alphabet has been
     * solved without errors, and the method returns true. If the arrays are not
     * empty, substitutes most frequent character with most common letter, and
     * checks for errors. If errors exist, tries to substitute the most frequent
     * with the next most common. If no errors exist, makes new arrays that are
     * subsets of the frequencyList and mostCommonList, excluding the letters
     * that have just been solved, and recursively calls itself to solve the
     * rest of the alpahbet. If this recursive call returns true, then a
     * solution has been found, and returns true. Otherwise, continues
     * iterating. If no letters can be found to substitute for the most frequent
     * character, returns false.
     */
    private boolean guesser(char[] frequencyList, char[] mostCommonList) {
    	
        if (frequencyList.length == 0) {
        	
        	System.out.println("\nDecrypted message:\n");
    		System.out.println(decryptedMessage);
    		return true;
    		
    	}
        
        int index=0;
        char[] subFreqList = new char[frequencyList.length-1],
                subCommonList = new char[mostCommonList.length-1];
        System.arraycopy(
                    frequencyList, 1, subFreqList, 0, subFreqList.length);
        System.arraycopy(
                    mostCommonList, 1, subCommonList, 0, subCommonList.length);
        char encryptedChar = frequencyList[0],
                charToReplace;
        
        while (index<mostCommonList.length) {
        	
            charToReplace = mostCommonList[index];
            swap(encryptedChar, charToReplace);

            //check for errors
            if (checker(charToReplace)) { //no errors found
            	
                System.arraycopy(mostCommonList, 0, subCommonList, 0, index);
                
                if (guesser(subFreqList, subCommonList)) {
                	
                	return true;
                	
                }
                
            }
            
            index++;
            
        }//End while
        
        swap(encryptedChar, '_');
        return false;
    }//End method
    
    /* swap method
     * Accepts as parameters the character that is being decrypted, and the
     * letter that is the most likely decryption of that character. Looks
     * through the encrypted message character by character. If the specified
     * character is found, notes the index, and replaces the character at the
     * same index in the decrypted message with the specified letter in the
     * correct case.
     */
    private void swap(char toSwap, char decryptionGuess) {
    	
        char[] temp = decryptedMessage.toCharArray();
        
        for (int i=0;i<encryptedMessage.length();i++) {
        	
            if (Character.toLowerCase(encryptedMessage.charAt(i))==Character.toLowerCase(toSwap)) {
            	
                if (Character.isLowerCase(encryptedMessage.charAt(i))) {
                	
                    temp[i]=Character.toLowerCase(decryptionGuess);
                    
                }
                else {
                	
                    temp[i]=Character.toUpperCase(decryptionGuess);
                    
                }
                
            }
            
        }
        
        decryptedMessage = String.valueOf(temp);
    }
    
    /* checker method
     * Accepts as parameter the character which has just been added to the
     * decrypted message. Looks through all of the words in the decrypted
     * message, and collects all of the completed words that contain the
     * specified character. These words must have just been completed, and
     * therefore are the newest completed words. If any of these words are not
     * in the dictionary, returns false. Returns true if no errors are found.
     */
    private boolean checker(char c) {
    	
        //get list of words to check
        Stack<String> finishedWords = new Stack<String>();
        StringTokenizer text = new StringTokenizer(decryptedMessage," .,;:\"/&!?\n");
        String nextWord;
        
        while(text.hasMoreTokens()) {
        	
            nextWord=text.nextToken();
            
            if (nextWord.contains(c+"")&&!nextWord.contains("_")) {
            	
                finishedWords.add(nextWord);
                
            }
            
        }
        
        while(!finishedWords.empty()) {
        	
            nextWord=finishedWords.pop();
            
            if (!dictionary.contains(nextWord)) {
            	
                //System.out.println("Dictionary does not include '"+nextWord+"'.");
                return false;
                
            }
            
        }
        
        return true;
    }
    
    /*
     * Creates a simple gui window with a scrollable text area
     * that displays the output messages.
     */
    private void createAndShowGUI() {
    	
        JFrame frame = new JFrame("Decrypter");
        JPanel thePanel = new JPanel(new BorderLayout());
        
        textArea = new JTextArea("Choose message to decrypt");
        textArea.setEditable(false);
        textArea.setLineWrap(true);
        textArea.addMouseListener(this);
        mouseListenerIsActive = true;
        
        JScrollPane scrollPane = new JScrollPane(textArea);
        
        thePanel.add(scrollPane);
    	thePanel.setOpaque(true);
    	
        frame.setContentPane(thePanel);
        frame.setLocationByPlatform(true);
        frame.setPreferredSize(new Dimension(450,450));
        
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
        
    }
    
    public static void main(String[] args) {
    	
    	Main theDecrypter = new Main();
    	theDecrypter.beginDecryption();
    	
    }
    
    /*
     * The following two methods are used to redirect the System.out.println
     * method to the text are of the gui.
     */
    private void updateTextArea(final String text) {
    	SwingUtilities.invokeLater(new Runnable() {
    		public void run() {
    			textArea.append(text);
			}
		});
	}
    
    private void redirectSystemStreams() {
    	OutputStream out = new OutputStream() {
    		@Override
    		public void write(int b) throws IOException {
    			updateTextArea(String.valueOf((char) b));
			}
    		@Override
    		public void write(byte[] b, int off, int len) throws IOException {
    			updateTextArea(new String(b, off, len));
			}
    		@Override
    		public void write(byte[] b) throws IOException {
    			write(b, 0, b.length);
			}
		};
		System.setOut(new PrintStream(out, true));
		System.setErr(new PrintStream(out, true));
	}

	@Override
	public void mouseClicked(MouseEvent me) {
		if (mouseListenerIsActive) {
			System.out.println(decryptedMessage);
			textArea.setCaretPosition(textArea.getDocument().getLength());
		}
	}

	@Override
	public void mouseEntered(MouseEvent me) {}

	@Override
	public void mouseExited(MouseEvent me) {}

	@Override
	public void mousePressed(MouseEvent me) {}

	@Override
	public void mouseReleased(MouseEvent me) {}
    
}
